﻿/*  
	------------------------------------------------------------------------
	 NQ Core Library
	
	 Homepage: http://www.awzhome.de/
	------------------------------------------------------------------------
	
	This Source Code Form is subject to the terms of the Mozilla Public
	License, v. 2.0. If a copy of the MPL was not distributed with this
	file, You can obtain one at http://mozilla.org/MPL/2.0/.
   
	The Original Code is code of NQ Core Library.

	The Initial Developer of the Original Code is Andreas Weizel.
	Portions created by the Initial Developer are
	Copyright (C) 2012 Andreas Weizel. All Rights Reserved.
	
	Contributor(s): (none)
	
	------------------------------------------------------------------------
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;

namespace AWZhome.NQ.Core
{
	/// <summary>
	/// NQ Core's default implementation of a service instance creator.
	/// </summary>
	/// <remarks>
	/// This creator works by analyzing the given service using reflection
	/// and then compiling code fragments, which later create requested
	/// service instances. Compiling is performed "lazily" on demand.
	/// </remarks>
	internal class ServiceInstanceCreator : INQServiceInstanceCreator
	{
		private Dictionary<string, Func<object[], object>> _parametrizedCreators = new Dictionary<string, Func<object[], object>>();
		private INQServiceInfo _serviceInfo;

		public void Initialize(INQServiceInfo serviceInfo)
		{
			_parametrizedCreators.Clear();
			_serviceInfo = serviceInfo;
		}

		/// <summary>
		/// Generates and compiles a new creator lambda expression for the given service.
		/// </summary>
		/// <param name="cctorParamTypes">Types of the parameters passed to the service directly from code
		/// (e.g. through <see cref="AWZhome.NQ.Core.INQServiceManager.CreateDependentService#T"/>).</param>
		private Func<object[], object> GenerateCompiledCreator(Type[] cctorParamTypes)
		{
			INQServiceInfo servInfo = _serviceInfo;
			string generatorKey = this.CreateParametrizedCreatorKey(cctorParamTypes);
			if (_parametrizedCreators.ContainsKey(generatorKey))
			{
				return _parametrizedCreators[generatorKey];
			}

			NQServiceManager.Instance.LogMessage(NQLogType.Debug, "Begin compiling creator for " + servInfo.ServiceInterface.ToString());

			if (((servInfo.AutoInjections == null) || (servInfo.AutoInjections.Length == 0))
				&& (((cctorParamTypes == null) || (cctorParamTypes.Length == 0))))
			{
				// Check whether we have an empty constructor in created class
				ConstructorInfo cctor = _serviceInfo.ServiceType.GetConstructor(new Type[] { });
				if (cctor == null)
				{
					// No suitable constructor in service implementation class
					string message = String.Format("Service implementation for {0} does not offer a constructor for given parameters.",
						_serviceInfo.ServiceInterface.Name);
					NQServiceManager.Instance.LogMessage(NQLogType.Error, "Unsuitable constructor: " + message);
					throw new NQServiceLoadingException(NQErrorReason.NoConstructor, message);
				}

				// We need a constructor without parameters, we can get it without dynamic compiling.
				MethodInfo getSimpleCreatorMethodGeneric =
					typeof(ServiceInstanceCreator).GetMethod("GetSimpleCreator", BindingFlags.Instance | BindingFlags.NonPublic);
				MethodInfo getSimpleCreatorMethod =
					getSimpleCreatorMethodGeneric.MakeGenericMethod(new Type[] { servInfo.ServiceType });
				Func<object[], object> simpleCreator = (Func<object[], object>) getSimpleCreatorMethod.Invoke(this, new object[] { });
				_parametrizedCreators.Add(generatorKey, simpleCreator);

				NQServiceManager.Instance.LogMessage(NQLogType.Debug, "End compiling (simple) creator for " + servInfo.ServiceInterface.ToString());

				return simpleCreator;
			}
			else
			{
				// Complex constructor of service class: Use DLR functionality to compile a creator delegate for it
				List<Expression> expressionList = new List<Expression>();
				List<ParameterExpression> declarationList = new List<ParameterExpression>();
				List<ParameterExpression> paramExpressions = new List<ParameterExpression>();

				/* Construct following dynamic method:
				 * delegate()
				 * {
				 *		var manager = NQServiceManager.Instance;
				 *		var autoInjected1 = manager.GetAttachedServices(autoInjection.BoundAttachList), true, false);
				 *			...
				 *		var autoInjected[n] = manager.CreateService(autoInjection.BoundService);
				 *		return new [ServiceType]([params]);
				 *	}
				 */

				ParameterExpression managerVarExpr = Expression.Parameter(typeof(INQServiceManager), "manager");
				declarationList.Add(
						managerVarExpr
					);
				expressionList.Add(
						Expression.Assign(managerVarExpr, Expression.Property(null, typeof(NQServiceManager).GetProperty("Instance")))
					);

				ParameterExpression creatorParameterExpr = Expression.Parameter(typeof(object[]), "dynamicParams");

				if ((cctorParamTypes != null) && (cctorParamTypes.Length > 0))
				{
					// Create ParameterExpressions from the given dynamic parameter types
					int dynamicParamCounter = 0;
					paramExpressions.AddRange(
							cctorParamTypes.Select<Type, ParameterExpression>(
								type => Expression.Parameter(type, "cctorParam" + (dynamicParamCounter++).ToString())
							)
						);

					for (int i = 0; i < cctorParamTypes.Length; i++)
					{
						// Create an assigment of parameter
						expressionList.Add(
								Expression.IfThenElse(
									Expression.And(
											Expression.LessThan(
													Expression.Constant(i),
													Expression.Property(creatorParameterExpr, typeof(object[]).GetProperty("Length"))
												),
											Expression.TypeIs(
													Expression.ArrayIndex(
															creatorParameterExpr,
															Expression.Constant(i)
														),
													cctorParamTypes[i]
												)
										),
									Expression.Assign(
											paramExpressions[i],
											Expression.Convert(
												Expression.ArrayIndex(
														creatorParameterExpr,
														Expression.Constant(i)
													),
													cctorParamTypes[i]
												)
										),
									Expression.Assign(
											paramExpressions[i],
											Expression.Constant(null, cctorParamTypes[i])
										)
								)
							);
					}
				}

				// Collect auto-injected services
				if (servInfo.AutoInjections != null)
				{
					for (int j = 0; j < servInfo.AutoInjections.Length; j++)
					{
						INQAutoInjection autoInjection = servInfo.AutoInjections[j];
						if (autoInjection.Interface != null)
						{
							if (autoInjection.InjectFromAttachList)
							{
								MethodInfo getAttachedServicesMethodGeneric = typeof(INQServiceManager).GetMethod("GetAttachedServices",
									new Type[] { typeof(string), typeof(bool), typeof(bool) });
								MethodInfo getAttachedServicesMethod =
									getAttachedServicesMethodGeneric.MakeGenericMethod(new Type[] { autoInjection.Interface });

								// Create parameter expressions
								ParameterExpression instancesExpr =
									Expression.Parameter(autoInjection.Interface.MakeArrayType(), "autoInjected" + j.ToString());
								paramExpressions.Add(instancesExpr);
								expressionList.Add(
										Expression.Assign(
											instancesExpr,
											Expression.Call(
												managerVarExpr,
												getAttachedServicesMethod,
												new Expression[]
												{
													Expression.Constant(autoInjection.BoundAttachList, typeof(string)),
													Expression.Constant(true, typeof(bool)),
													Expression.Constant(false, typeof(bool))
												}
											)
										)
									);
							}
							else
							{
								if (!autoInjection.InjectAsDependent)
								{
									if (autoInjection.Overridable && (paramExpressions.Count > 0))
									{
										// When overriding, use the first of the passed dependent instances instead of creating one
										paramExpressions.Add(paramExpressions[0]);
										paramExpressions.RemoveAt(0);
									}
									else
									{
										// Find appropriate CreateService() using reflection, because we don't know the
										// generic type parameter at compile time.
										MethodInfo[] createDependentServiceGenericMethods = typeof(INQServiceManager).GetMethods();
										for (int i = 0; i < createDependentServiceGenericMethods.Length; i++)
										{
											if ((createDependentServiceGenericMethods[i].Name == "CreateService")
												&& createDependentServiceGenericMethods[i].IsGenericMethod)
											{
												MethodInfo createDependentServiceMethod =
													createDependentServiceGenericMethods[i].MakeGenericMethod(
													new Type[] { autoInjection.Interface });

												// Create parameter expressions
												ParameterExpression instancesExpr =
													Expression.Parameter(autoInjection.Interface, "autoInjected" + j.ToString());
												paramExpressions.Add(instancesExpr);
												expressionList.Add(
														Expression.Assign(
															instancesExpr,
															Expression.Call(
																managerVarExpr,
																createDependentServiceMethod,
																Expression.Constant(autoInjection.BoundService, typeof(Type))
															)
														)
													);
											}
										}
									}
								}
							}
						}
					}
				}

				// Add the expression for creation of the main instance
				ConstructorInfo cctor = servInfo.ServiceType.GetConstructor(
						paramExpressions.Select<ParameterExpression, Type>(p => p.Type).ToArray()
					);

				if (cctor != null)
				{
					expressionList.Add(
							Expression.New(cctor, paramExpressions.ToArray())
						);

					// Compile the expression
					List<ParameterExpression> allDeclarations = new List<ParameterExpression>();
					allDeclarations.AddRange(paramExpressions);
					allDeclarations.AddRange(declarationList);
					List<Expression> blockExpressions = new List<Expression>();
					//blockExpressions.AddRange(dynamicParamValueAssignments);
					blockExpressions.AddRange(expressionList);
					BlockExpression creatorBlock = Expression.Block(allDeclarations.ToArray(), blockExpressions);
					Func<object[], object> compiledCreator =
						Expression.Lambda<Func<object[], object>>(creatorBlock, creatorParameterExpr).Compile();
					_parametrizedCreators.Add(generatorKey, compiledCreator);

					NQServiceManager.Instance.LogMessage(NQLogType.Debug, "End compiling creator for " + servInfo.ServiceInterface.ToString());

					return compiledCreator;
				}
				else
				{
					// No suitable constructor in service implementation class
					string message = String.Format("Service implementation for {0} does not offer a constructor for given parameters.",
						_serviceInfo.ServiceInterface.Name);
					NQServiceManager.Instance.LogMessage(NQLogType.Error, "Unsuitable constructor: " + message);
					throw new NQServiceLoadingException(NQErrorReason.NoConstructor, message);
				}
			}
		}

		/// <summary>
		/// Creates a key for the hash table of creators from an
		/// <see cref="AWZhome.NQ.Core.INQServiceInfo">INQServiceInfo</see> and the list of the
		/// passed parameter types.
		/// </summary>
		/// <param name="cctorParamTypes">Array of types of the passed parameters.</param>
		/// <returns>Created key as string.</returns>
		private string CreateParametrizedCreatorKey(Type[] cctorParamTypes)
		{
			string key = _serviceInfo.ServiceInterface.FullName + "|";
			if ((cctorParamTypes != null) && (cctorParamTypes.Length > 0))
			{
				key += String.Join("|", cctorParamTypes.Select<Type, string>(t => t.FullName));
			}

			return key;
		}

		/// <summary>
		/// Creates an instance of the service.
		/// </summary>
		/// <typeparam name="T">Service type.</typeparam>
		/// <param name="cctorParamTypes">Types of the dynamic constructor parameters to pass.</param>
		/// <param name="cctorParams">Dynamic constructor parameters to pass.</param>
		/// <returns>Service object or <c>null</c>, if the ServiceInstanceCreator is not
		/// valid or does not support the given service type <typeparamref name="T"/>.</returns>
		public T CreateInstance<T>(Type[] cctorParamTypes, object[] cctorParams)
		{
			Func<object[], object> compiledCreator = this.GenerateCompiledCreator(cctorParamTypes);
			if (compiledCreator != null)
			{
				object rawCreatedInstance = compiledCreator(cctorParams);
				if (rawCreatedInstance is T)
				{
					T createdInstance = (T) rawCreatedInstance;
					return createdInstance;
				}
				else
				{
					// Throw an NQServiceLoadingException
					string message = String.Format("Service {0} does not implement type {1}.", _serviceInfo.ServiceInterface.Name, typeof(T).Name);
					NQServiceManager.Instance.LogMessage(NQLogType.Error, "Unsupported interface type: " + message);
					throw new NQServiceLoadingException(NQErrorReason.UnsupportedInterfaceType, message);
				}
			}

			return default(T);
		}

		private Func<object[], object> GetSimpleCreator<T>()
			where T : new()
		{
			return (a) => new T();
		}
	}
}
